////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//                                                                                                                            //
//                                                         WNProject                                                          //
//                                                                                                                            //
//            This file is distributed under the BSD 2-Clause open source license. See License.txt for details.               //
//                                                                                                                            //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

#ifndef __WN_THREAD_INL__
#define __WN_THREAD_INL__

#include "WNCore/inc/WNBase.h"
#include "WNCore/inc/WNAssert.h"
#include "WNMemory/inc/WNMemory.h"

#ifdef _WN_WINDOWS
    #include <windows.h>
#elif defined _WN_LINUX
    #include "WNCore/inc/WNMacros.h"

    #include <unistd.h>
    #include <pthread.h>
    #include <time.h>
    #include <sys/syscall.h>
    #include <errno.h>
#endif

__WN_INLINE WN_UINT32 WNThreadCreate(WN_THREAD_T* _thread, WN_THREAD_FUNCTION_T _startRoutine, WN_VOID* _parameter, WN_SIZE_T _stackSize) {
    WN_DEBUG_ASSERT(_thread != WN_NULL);

    if (_thread != WN_NULL) {
        #ifdef _WN_WINDOWS
            const HANDLE thread = CreateThread(WN_NULL, _stackSize, (LPTHREAD_START_ROUTINE)_startRoutine, _parameter, 0, WN_NULL);

            if (thread == WN_NULL) {
                return(WN_FAILED);
            }

            *_thread = thread;
        #elif defined _WN_LINUX
            pthread_attr_t attributes;

            if (pthread_attr_init(&attributes) != 0) {
                return(WN_FAILED);
            } else {
                if (_stackSize > 0) {
                    if (pthread_attr_setstacksize(&attributes, _stackSize) != 0) {
                        pthread_attr_destroy(&attributes);

                        return(WN_FAILED);
                    }
                }

                pthread_t thread;

                if (pthread_create(&thread, &attributes, _startRoutine, _parameter) != 0) {
                    return(WN_FAILED);
                }

                pthread_attr_destroy(&attributes);

                *_thread = thread;
            }
        #endif
    } else {
        return(WN_FAILED);
    }

    return(WN_SUCCEEDED);
}

__WN_INLINE WN_THREAD_T WNThreadSelf() {
    #ifdef _WN_WINDOWS
        return(GetCurrentThread());
    #elif defined WN_LINUX
        return(pthread_self());
    #endif
}

__WN_INLINE WN_VOID WNThreadYield() {
    #ifdef _WN_WINDOWS
        Sleep(0);
    #elif defined _WN_LINUX
        pthread_yield();
    #endif
}

__WN_INLINE WN_VOID WNThreadSleep(WN_UINT32 _milliseconds) {
    #ifdef _WN_WINDOWS
        Sleep(_milliseconds);
    #elif defined _WN_LINUX
        struct timespec timeRequested = {0};

        timeRequested.tv_sec = _milliseconds / 1000;
        timeRequested.tv_nsec = (_milliseconds % 1000) * 1000000L;

        while (nanosleep(&timeRequested, &timeRequested) == -1) {
            continue;
        }
    #endif
}

__WN_INLINE WN_VOID WNThreadExit(WN_UINT32 _return) {
    #ifdef _WN_WINDOWS
        ExitThread(_return);
    #elif defined _WN_LINUX
        pthread_exit((WN_VOID*)_return);
    #endif
}

__WN_INLINE WN_UINT32 WNThreadSetAffinity(WN_THREAD_T _thread, WN_SIZE_T _affinity) {
    #ifdef _WN_WINDOWS
        WN_DEBUG_ASSERT(_thread != WN_NULL);

        if (_thread != WN_NULL) {
            if (SetThreadAffinityMask(_thread, _affinity) == 0) {
                return(WN_FAILED);
            }
        } else {
            return(WN_FAILED);
        }
    #elif defined _WN_LINUX
        cpu_set_t affinity;

        CPU_ZERO(&affinity);

        for (WN_SIZE_T i = 0; i < WN_BYTES_TO_BITS(sizeof(WN_SIZE_T)); i++) {
            _affinity >>= i;

            if ((_affinity & 1) == 1) {
                CPU_SET(i, &affinity);
            }
        }

        if (pthread_setaffinity_np(_thread, sizeof(cpu_set_t), &affinity) != 0) {
            return(WN_FAILED);
        }
    #endif

    return(WN_SUCCEEDED);
}

__WN_INLINE WN_UINT32 WNThreadSetPriority(WN_THREAD_T _thread, WN_INT32 _priority) {
    #ifdef _WN_WINDOWS
        WN_DEBUG_ASSERT(_thread != WN_NULL);

        if (_thread != WN_NULL) {
            const WN_BOOL result = SetThreadPriority(_thread, _priority) != 0 ? WN_TRUE : WN_FALSE;

            if (result == WN_FALSE) {
                return(WN_FAILED);
            }
        } else {
            return(WN_FAILED);
        }
    #elif defined _WN_LINUX
        return(WN_FAILED); // need to figure out
    #endif

    return(WN_SUCCEEDED);
}

__WN_INLINE WN_UINT32 WNThreadGetPriority(WN_THREAD_T _thread, WN_UINT32* _priority) {
    WN_DEBUG_ASSERT(_priority != WN_NULL);

    if (_priority != WN_NULL) {
        #ifdef _WN_WINDOWS
            WN_DEBUG_ASSERT(_thread != WN_NULL);

            if (_thread != WN_NULL) {
                const WN_UINT32 result = GetThreadPriority(_thread);

                if (result == THREAD_PRIORITY_ERROR_RETURN) {
                    return(WN_FAILED);
                }

                *_priority = result;
            } else {
                return(WN_FAILED);
            }
        #elif defined _WN_LINUX
            return(WN_SUCCEEDED); // need to figure out
        #endif
    } else {
        return(WN_FAILED);
    }

    return(WN_SUCCEEDED);
}

__WN_INLINE WN_THREAD_ID_T WNThreadID() {
    #ifdef _WN_WINDOWS
        return(GetCurrentThreadId());
    #elif defined _WN_LINUX
        return((WN_THREAD_ID_T)syscall(SYS_gettid));
    #endif
}

__WN_INLINE WN_UINT32 WNThreadGetExitCode(WN_THREAD_T _thread, WN_UINT32* _return) {
    WN_DEBUG_ASSERT(_return != WN_NULL);

    if (_return != WN_NULL) {
        #ifdef _WN_WINDOWS
            WN_DEBUG_ASSERT(_thread != WN_NULL);

            if (_thread != WN_NULL) {
                if (GetExitCodeThread(_thread, (LPDWORD)_return) == STILL_ACTIVE) {
                    return(WN_FAILED);
                }
            } else {
                return(WN_FAILED);
            }
        #elif defined _WN_LINUX
            if (pthread_tryjoin_np(_thread, (WN_VOID**)&_return) == EBUSY) {
                return(WN_FAILED);
            }
        #endif
    } else {
        return(WN_FAILED);
    }

    return(WN_SUCCEEDED);
}

__WN_INLINE WN_UINT32 WNWaitForSingleThread(WN_THREAD_T _thread) {
    WN_DEBUG_ASSERT(_thread != WN_NULL);

    return(WNWaitForSingleThreadTimed(_thread, WN_THREAD_INFINITE_TIMEOUT));
}

__WN_INLINE WN_UINT32 WNWaitForSingleThreadTimed(WN_THREAD_T _thread, WN_UINT32 _milliseconds) {
    WN_DEBUG_ASSERT(_thread != WN_NULL);
    WN_DEBUG_ASSERT(_milliseconds != 0);

    #ifdef _WN_WINDOWS
        if (WaitForSingleObject(_thread, _milliseconds) == WAIT_FAILED) {
            return(WN_FAILED);
        }
    #elif defined _WN_LINUX
        if (_milliseconds >= WN_THREAD_INFINITE_TIMEOUT) {
            if (pthread_join(_thread, WN_NULL) != 0) {
                return(WN_FAILED);
            }
        } else {
            struct timespec timeout = {0};

            timeout.tv_sec = _milliseconds / 1000;
            timeout.tv_nsec = (_milliseconds % 1000) * 1000000L;

            if (pthread_timedjoin_np(_thread, WN_NULL, &timeout) != 0) {
                return(WN_FAILED);
            }
        }
    #endif

    return(WN_SUCCEEDED);
}

__WN_INLINE WN_UINT32 WNWaitForMultipleThreads(const WN_THREAD_T* _threads, WN_UINT32 _count) {
    WN_DEBUG_ASSERT(_threads != WN_NULL);
    WN_DEBUG_ASSERT(_count != 0);

    return(WNWaitForMultipleThreadsTimed(_threads, _count, WN_THREAD_INFINITE_TIMEOUT));
}

__WN_INLINE WN_UINT32 WNWaitForMultipleThreadsTimed(const WN_THREAD_T* _threads, WN_UINT32 _count, WN_UINT32 _milliseconds) {
    WN_DEBUG_ASSERT(_threads != WN_NULL);
    WN_DEBUG_ASSERT(_count != 0);
    WN_DEBUG_ASSERT(_milliseconds != 0);

    #ifdef _WN_WINDOWS
        if (WaitForMultipleObjects(_count, _threads, WN_TRUE, _milliseconds) == WAIT_FAILED) {
            return(WN_FAILED);
        }
    #elif defined _WN_LINUX
        for (WN_UINT32 i = 0; i < _count; i++) {
            if (WNWaitForSingleThreadTimed(_threads[i], _milliseconds) == WN_FAILED) {
                return(WN_FAILED);
            }
        }
    #endif

    return(WN_SUCCEEDED);
}

__WN_INLINE WN_UINT32 WNThreadCleanup(WN_THREAD_T _thread) {
    #ifdef _WN_WINDOWS
        WN_DEBUG_ASSERT(_thread != WN_NULL);

        if (CloseHandle(_thread) == 0) {
            return(WN_FAILED);
        }
    #endif

    return(WN_SUCCEEDED);
}

#endif // __WN_THREAD_INL__
